feat: Read-State Awareness — eliminate fabricated continuity language in symbolic moment reads#474
feat: Read-State Awareness — eliminate fabricated continuity language in symbolic moment reads#474
Conversation
Agent-Logs-Url: https://github.com/DHCross/Shipyard/sessions/765b5c8b-62ad-481b-8d6e-61021c7ef338 Co-authored-by: DHCross <45954119+DHCross@users.noreply.github.com>
- Add readState.ts with ReadState types, computeReadState, computeContinuityDiff, generateContinuityLanguage, and validateReadStateConsistency - Add readHistory.ts with Firestore-backed appendRead/getMostRecentRead/getReadsForDate - Update symbolicMomentComposer.ts: suppress "Changed since last check" block on first reads (when readState is null or is_first_read_ever/is_first_read_today) - Update symbolicMomentFrontstage.ts: thread readState through buildStructuredSymbolicMomentReply, update validators to accept readState and gate the continuity block requirement, extend SealedSymbolicMomentAudit with read_state snapshot - Wire read state in route.ts: compute readState before generation, append read history entry after successful (non-void) symbolic moment delivery - Add 17 regression tests in readState.test.ts covering all read state scenarios - Update symbolicMomentFrontstage.test.ts: flip the fabricated "No new signal" assertion to correctly verify absence of continuity block on first reads - Add both new test files to the smoke test suite in package.json Agent-Logs-Url: https://github.com/DHCross/Shipyard/sessions/765b5c8b-62ad-481b-8d6e-61021c7ef338 Co-authored-by: DHCross <45954119+DHCross@users.noreply.github.com>
- Simplify null check: previousDelta == null instead of explicit triple - Extract isFirstReadOrDefault helper to eliminate repeated null-check pattern - Use isFirstReadOrDefault in both structural validators (no more inline ternary for null/undefined defaulting) - Extract ASPECT_LABEL_PATTERN as a named constant in route.ts - Use crypto.randomUUID() for read_id instead of Date.now() + Math.random() Agent-Logs-Url: https://github.com/DHCross/Shipyard/sessions/765b5c8b-62ad-481b-8d6e-61021c7ef338 Co-authored-by: DHCross <45954119+DHCross@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds read-state awareness to Symbolic Moment reads so first-time reads no longer fabricate “Changed since last check” language, while continuation reads can include real diffs (and persist/read history via Firestore).
Changes:
- Introduces pure domain logic for read-state + continuity diffs (
readState.ts) and Firestore-backed persistence (readHistory.ts). - Threads
readStatethrough Symbolic Moment composition/validation and records a snapshot in the sealed audit payload. - Wires the API route to compute read-state from persisted history and append a read entry post-delivery; updates/extends smoke tests.
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| vessel/src/lib/server/readHistory.ts | New Firestore persistence helpers for read history entries (append + query). |
| vessel/src/lib/raven/readState.ts | New pure read-state + continuity diff computation and validation helpers. |
| vessel/src/lib/raven/symbolicMomentFrontstage.ts | Threads readState through validators and audit sealing; adjusts continuity-block requirements. |
| vessel/src/lib/raven/symbolicMomentComposer.ts | Makes continuity block nullable and suppressible based on readState. |
| vessel/src/app/api/raven-chat/route.ts | Computes chart fingerprint, loads read history, passes readState, and appends read entries. |
| vessel/src/lib/raven/tests/symbolicMomentFrontstage.test.ts | Updates assertions to reflect first-read continuity suppression and read_state snapshot. |
| vessel/src/lib/raven/tests/readState.test.ts | New regression coverage for read-state/diff/validator scenarios. |
| vessel/package.json | Adds new tests to smoke suite. |
| vessel/sherlog-velocity/data/self-model.json | Regenerated Sherlog model metadata. |
| vessel/sherlog-velocity/data/gap-history.jsonl | Appends latest gap-history record. |
| .orderBy('timestamp_utc', 'asc') | ||
| .get(); | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| return snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry); |
There was a problem hiding this comment.
getReadsForDate() uses where('user_local_date', '==', localDate).orderBy('timestamp_utc'). In Firestore this typically requires a composite index (user_local_date + timestamp_utc); without it the query throws and you silently return [], which will make computeReadState() treat continuations as first reads and suppress continuity indefinitely. Consider adding the required index to repo-managed Firestore indexes, or adjust the query/storage shape to avoid needing a composite index.
| .orderBy('timestamp_utc', 'asc') | |
| .get(); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| return snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry); | |
| .get(); | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| const entries = snap.docs.map((doc: any) => doc.data() as ReadHistoryEntry); | |
| return entries.sort((a, b) => a.timestamp_utc.localeCompare(b.timestamp_utc)); |
|
|
||
| export type AspectChange = { | ||
| aspect: AspectSnapshot; | ||
| delta: number; // positive = orb narrowed (tightening), negative = orb widened (loosening) |
There was a problem hiding this comment.
AspectChange.delta is documented as “positive = tightening, negative = loosening”, but computeContinuityDiff() always stores a positive magnitude and uses separate tightening/loosening arrays. Please align the comment/type with the actual representation (or store negative deltas for loosening).
| delta: number; // positive = orb narrowed (tightening), negative = orb widened (loosening) | |
| delta: number; // non-negative orb-change magnitude; direction is conveyed by membership in tightening vs loosening |
| // Nothing material changed | ||
| if ( | ||
| diff.tightening.length === 0 && | ||
| diff.loosening.length === 0 && | ||
| diff.new_aspects.length === 0 && | ||
| !diff.moon_sign_changed | ||
| ) { | ||
| if (diff.minutes_elapsed < 30) { | ||
| return `${elapsed} on, the field is essentially where it was. There isn't new news, just confirmation of the line.`; | ||
| } | ||
| return `The field is steady — what was here ${elapsed} ago is still here.`; | ||
| } | ||
|
|
There was a problem hiding this comment.
generateContinuityLanguage()'s “Nothing material changed” early-return ignores departed_aspects and chamber_shifted. As a result, an aspect leaving range (or chamber changing) will incorrectly produce a “field is steady” message, and the later departed_aspects branch becomes unreachable in that scenario. Include departed_aspects.length and chamber_shifted in the early-return condition (or handle them before returning).
| if (!firstRead) { | ||
| const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block)); | ||
| if (!changeBlock) { | ||
| return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' }; | ||
| } |
There was a problem hiding this comment.
On first reads the continuity block is supposed to be absent, but when firstRead is true this validator only skips requiring the "Changed since last check" block; it does not fail if the block is present. That means fabricated continuity could still pass validation if it appears in Zone 1. Consider explicitly rejecting changeBlock when firstRead is true (and similarly in validateInternalSymbolicMomentStructure).
| if (!firstRead) { | |
| const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block)); | |
| if (!changeBlock) { | |
| return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' }; | |
| } | |
| const changeBlock = blocks.find((block) => /^\*\*Changed since last check:\*\*/i.test(block)); | |
| if (firstRead) { | |
| if (changeBlock) { | |
| return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' }; | |
| } | |
| } else if (!changeBlock) { | |
| return { passes: false, failureType: 'STRUCTURAL_TEMPLATE_VIOLATION' }; |
| // Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction. | ||
| const ASPECT_LABEL_PATTERN = /^([A-Za-z\s]+)\s+(square|opposition|conjunction|trine|sextile|quincunx|semisquare|semisextile|sesquiquadrate)\s+([A-Za-z\s]+)$/i; |
There was a problem hiding this comment.
ASPECT_LABEL_PATTERN here supports a narrower set of aspect keywords than the rest of the symbolic-moment pipeline (e.g. symbolicMomentFrontstage parses conjunct and inconjunct). If astrology.anchor.topDrivers[].label can contain those variants, they’ll be silently dropped from primary_aspects, weakening continuity diffs. Consider reusing the same aspect grammar/constants as the driver parser, or expanding this regex to match the full supported vocabulary.
| // Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction. | |
| const ASPECT_LABEL_PATTERN = /^([A-Za-z\s]+)\s+(square|opposition|conjunction|trine|sextile|quincunx|semisquare|semisextile|sesquiquadrate)\s+([A-Za-z\s]+)$/i; | |
| const ASPECT_LABEL_KEYWORDS = [ | |
| 'square', | |
| 'opposition', | |
| 'conjunction', | |
| 'conjunct', | |
| 'trine', | |
| 'sextile', | |
| 'quincunx', | |
| 'inconjunct', | |
| 'semisquare', | |
| 'semisextile', | |
| 'sesquiquadrate', | |
| ] as const; | |
| // Matches "Body1 aspect Body2" driver labels for aspect snapshot extraction. | |
| const ASPECT_LABEL_PATTERN = new RegExp( | |
| `^([A-Za-z\\s]+)\\s+(${ASPECT_LABEL_KEYWORDS.join('|')})\\s+([A-Za-z\\s]+)$`, | |
| 'i', | |
| ); |
| * Documents are stored under: | ||
| * raven_read_history/{userId_chartId}/{readId} | ||
| * | ||
| * Indexed queries: | ||
| * - Most-recent read for a chart (getMostRecentRead) | ||
| * - All reads for a chart on a given local date (getReadsForDate) |
There was a problem hiding this comment.
The file header comment says documents live at raven_read_history/{userId_chartId}/{readId}, but the implementation writes to raven_read_history/{partition}/entries/{docId} (nested entries subcollection). Please update the comment (or the schema) so it matches the actual Firestore layout—this affects debugging and index setup.
|


Raven was generating
**Changed since last check:** No new signal.on every symbolic moment read regardless of whether any prior read existed — fabricating continuity on first reads and ignoring real diffs on continuation reads.New modules
readState.ts— Pure domain layer:ReadState,ReadHistoryEntry,ContinuityDifftypes + pure functionscomputeReadState,computeContinuityDiff,generateContinuityLanguage,validateReadStateConsistency,isFirstReadOrDefaultreadHistory.ts— Firestore-backed persistence per user/chart:appendRead,getMostRecentRead,getReadsForDate. Errors degrade silently; read path is never blockedBehavioral changes
Changed since last check: No new signal.(fabricated)Changed since last check: No new signal.(fabricated)Changed since last check: No new signal.(static)Changed since last check: No new signal.Wiring
route.ts— Before generation: derives stablechartFingerprintfrom birth data, fetches today's reads + most-recent in parallel, computesReadState, passes it tobuildStructuredSymbolicMomentReply. After successful delivery: appendsReadHistoryEntryto Firestore (fire-and-forget)symbolicMomentComposer.ts— Accepts optionalreadState; suppresses continuity block whenisFirstRead(readState)is truesymbolicMomentFrontstage.ts— ThreadsreadStatethroughbuildStructuredSymbolicMomentReply; both structural validators gate the continuity-block requirement viaisFirstReadOrDefault;SealedSymbolicMomentAudit(Zone 2) includes aread_statesnapshotValidator enforcement
Tests
17 new regression tests in
readState.test.tscovering all read state scenarios, orb tightening/loosening, Moon sign change detection, and both validator failure modes. ExistingsymbolicMomentFrontstage.test.tsupdated: the test that previously asserted fabricatedNo new signal.now assertsdoesNotMatch(/\*\*Changed since last check:\*\*/). Continuation tests explicitly passreadState: { is_continuation: true, ... }to exercise the correct path. Both files added to the smoke suite.